#! /usr/bin/python

'''
Created on Sep 5, 2010

@author: David

Requires 
pyinotify http://trac.dbzteam.org/pyinotify
pexpect http://www.noah.org/wiki/Pexpect#Download_and_Installation
'''
__author__ = "Falmarri <psychicsurgeon@gmail.com>"
__version__ = "0.9.1"


import sys, re
from os import path
from getpass import getpass
import atexit, threading

try:
    import pexpect, asyncore, pyinotify
except ImportError:
    print 'This script requires pexpect and pyinotify'
    sys.exit(0)

def goodbye():
    print "\nStopping..."

class SCP( object ):
    '''
    classdocs
    '''

    credentials = {}
    expect = ['The authenticity of host', 'Permission denied', 'password', 'not gonna get this one', pexpect.EOF, pexpect.TIMEOUT]

    def scp( self ):
        cmd = []
        if self. opts is not None and self.opts.recursive: cmd.append( '-r' )
        cmd.append( self.local )
        cmd.append( self.remote['all'] )
        print 'scp ', ' '.join( cmd )

        child = pexpect.spawn( 'scp', cmd )
        index = child.expect( SCP.expect )
        print child.before, child.after
        #print SCP.expect[index]
        if index == 0:
            child.sendline( 'yes' )
        elif index == 1:
            raise Exception
        elif index == 2:
            child.sendline( SCP.credentials[self.loginto] )
        elif index == 3:
            pass
        elif index == 4:
            '''Nothing was sent'''
        elif index == 5:
            raise Exception
        index = child.expect( pexpect.EOF )
        print child.before


    def mkdir( self ):
        if self.loginto not in SCP.credentials:
            raise Exception

        mkd = [self.loginto , "'mkdir " + self.local + "'"]

        mkdircmd = 'ssh ' + " ".join( mkd )

        child = pexpect.spawn( 'ssh', mkd )

        child.send( self.credentials[self.loginto] )


    def __init__( self, local, remote, opts=None ):
        '''
        Constructor
        '''
        self.loginto = remote['user'] + '@' + remote['host']

        if self.loginto not in SCP.credentials:
            self._pass_request()

        self.local = local
        self.remote = remote
        self.opts = opts

        #remote = re.compile(r"(?P<user>\w+)@(?P<host>(?:(?:(?:[0-9]){1,3}.?){4})|(?:(\w+.?)+)):(?P<dest>.*)").groupdict()

    def _pass_request( self ):
        password = getpass( "Enter password for " + self.loginto + ':' )
        SCP.credentials[self.loginto] = password



class OnChangeHandler( pyinotify.ProcessEvent ):

    def my_init( self, dir, wait=False ):
        self.directories = dir
        self.wait = wait

    def process_IN_CREATE( self, event ):
        if not event.dir: self.directories.pushRemoteSites( event.pathname )
        else:
            pass
            #_create_dir( '', self.remotes, event.pathname )

    def process_IN_MODIFY( self, event ):
        self.directories.pushRemoteSites( event.pathname )

    def process_IN_DELETE( self, event ):
        #if self.options.delete: _delete(self, locals[0], self.remotes)
        pass

def _push_single( local, remotes, pathname ):
    pass
    diff = pathname[len( local ):]
    for r in remotes[:]:
        r = dict( r )
        r['all'] = ( r['all'].rstrip( '/' ) + '/' if r['dest'] != '' else r['all'] ) + diff.lstrip( '/' )
        SCP( pathname, r ).scp()


class Watcher( object ):

    def __init__( self, dir, options ):


        self.watches = {}
 
        self.wm = pyinotify.WatchManager()  # Watch Manager
        self.mask = pyinotify.IN_CREATE | pyinotify.IN_MODIFY  # watched events

        self.notifier = pyinotify.AsyncNotifier( self.wm, OnChangeHandler( dir=dir, wait=options.wait ) )

        for x in dir.locals():
            self.watches[x] = self.wm.add_watch( x, self.mask, rec=True, auto_add=True )
            if options.sync:
                print 'Syncing Directories...'
                dir.pushRemoteSites( x )



    def run( self ):

        self.notifier.loop()


def optmain():
    '''Main to be used with python < 2.7'''

    description = """ """
    usage = "usage: %prog [options] local username@remote:(/)destination"
    version = "%prog v%s" % __version__

    parser = OptionParser( usage=usage , version=version , description = description)
    parser.add_option( "-c", "--config", dest="config", type="string",
                help="CONFIG file to store logins", metavar="CONFIG" )
    parser.add_option( "-s", "--no-sync", dest="sync", action="store_false",
                help="Don't sync all directories on startup", default=True )
    parser.add_option( "-x", "--exclude", dest="exclude", type="string" )
    parser.add_option( "-w", "--wait", dest="wait", action="store_true",
                help="Wait for user input before syncing. Excludes initial sync", default=False)


    options, args = parser.parse_args()

    ( x.strip() for x in args )

    if len( args ) < 2:
        parser.error( "Incorrect number of arguments" )

    remotere = re.compile( r"(?P<all>(?P<user>\w+)@(?P<host>(?:(?:(?:[0-9]){1,3}.?){4})|(?:([[0-9\w]+.?)+)):(?P<dest>.*))" )
    localre = re.compile( r"^(/?(?:(?<!@).(?!@))*)$" )


    "([\w/\\(?=[:space:])]*/?)"

    remoteSites = []
    localDirs = []

    for arg in args:
        if remotere.match( arg ):
            r = remotere.search( arg )
            remoteSites.append( r.groupdict() )
        elif localre.match( arg ):
            localDirs.append( arg )
        else:
            parser.error( "Incorrect argument: " + arg )

    if not localDirs or not remoteSites:
        parser.error( "Must include at least 1 local directory and 1 remote directory" )

    directories = Directories( localDirs, remoteSites )

    watch = Watcher( directories, options )



    watch.run()
    
    if options.wait:
        waitLoop()


def waitLoop():
    pass


def argmain():
    optmain()


class Directories( object ):

    credentials = {}
    expect = ['The authenticity of host', 'Permission denied', 'password', 'refused', pexpect.EOF, pexpect.TIMEOUT]

    def __init__( self, locals, remotes ):
        self.localDirs = []
        self.remoteSites = remotes

        for local in locals:
            self.localDirs.append( path.abspath( local ) )

    def locals( self ):
        return [x for x in self.localDirs]

    def pushRemoteSites( self, pathname, dir=False ):

        which = ''

        r = ''

        if dir or path.isdir( pathname ):
            r = '-r'

        for l in self.localDirs:
            if pathname.startswith( l ):
                which = path.relpath( pathname, path.dirname( l ) ) if not r else ''
                break
        for remote in self.remoteSites:
            loginto = ''.join( [remote['user'], '@', remote['host']] )
            if loginto in Directories.credentials:
                password = Directories.credentials[loginto]
            else:
                password = getpass( "Enter password for " + loginto + ':' )
                Directories.credentials[loginto] = password
            self.scp( r + ' ' + pathname + ' ' + remote['user'] + '@' + remote['host'] + ':' + path.join( remote['dest'], which ), password )



    def scp( self, cmd, password=None ):

        if password is None:
            password = getpass( "Enter password for " + cmd + ':' )
            Directories.credentials[self.loginto] = password

        print 'SCPing... scp ' + cmd
        child = pexpect.spawn( 'scp ' + cmd )

        index = child.expect( Directories.expect )
        if index == 0:
            child.sendline( 'yes' )
        elif index == 1:
            child.sendcontrol( 'c' )
            sys.exit( 0 )
        elif index == 2:
            child.sendline( password )
        elif index == 3:
            print 'Connection refused...'
            sys.exit(0)
        elif index == 4:
            print 'Unexpected end of line'
        elif index == 5:
            print 'Timed out...'
            raise Exception
        index = child.expect( ['denied', pexpect.EOF] )
        if index == 0:
            child.sendcontrol( 'c' )
            print 'Permission denied'
            print 'Command failed:', 'scp ' + cmd
            sys.exit( 0 )
        else:
            print child.before

atexit.register( goodbye )

if __name__ == '__main__':


    try:
        import argparse
        argmain()
    except ImportError:
        from optparse import OptionParser
        optmain()
